//
//  MainLoggerPanelVC.swift
//  Hermes
//
//  Created by FH on 2021/5/19.
//  Copyright © 2021 Agora. All rights reserved.
//

import Foundation

fileprivate enum LogInfoType : String {
    case custom = "custom"
    case app = "app"
    case api = "api"
    case delegate = "delegate"
    case block = "block"
    
    static func toList() -> [String] {
        return ["all", "api", "delegate", "other"]
    }
}

fileprivate class LogInfo {
    static let dateFormatter = DateFormatter()
    let index: Int
    let type: LogInfoType
    let dateTime: String
    let method: String
    let info: String
    let isStaticMethod: Bool
    let isSDKInfo: Bool
    let invokeTarget: String
    
    init(index: Int, _ type: LogInfoType, _ method: String, _ info: String,
         _ isStaticMethod: Bool, _ isSDKInfo: Bool, _ invokeTarget: String) {
        if Self.dateFormatter.dateFormat.isEmpty {
            Self.dateFormatter.dateFormat = "MM-dd HH:mm:ss"
        }
        self.index = index
        self.type = type
        self.method = method
        self.info = info
        self.dateTime = Self.dateFormatter.string(from: Date())
        self.isStaticMethod = isStaticMethod
        self.isSDKInfo = isSDKInfo
        self.invokeTarget = invokeTarget
    }
    
    func toDataItem() -> [String : String] {
        return [
            "index": "\(index)",
            "type": type.rawValue,
            "time": dateTime,
            "method": method,
            "info": info
        ]
    }
}

extension LogInfo : CustomStringConvertible {
    var description: String {
        if type == .api {
            return "[\(index)] call \(invokeTarget).\(method) ==> \(info)"
        } else {
            return "[\(index)] \(isStaticMethod ? "+" : "-") \(method) withArgs: \(info)"
        }
    }
}

extension Array where Element == LogInfo {
    func toDataItems() -> [[String : String]] {
        var items = [[String : String]]()
        for info in self {
            items.append(info.toDataItem())
        }
        return items
    }
}

///////////////////////////////////////////////

class MainLoggerPanelVC : FLViewController {
    private let appContext = HermesContext()
    private let headerView = NSView()
    private let bodyView = NSView()
    // head
    private let btnTabScript = NSButton()
    private let btnTabLogger = NSButton()
    private let selectShowType = NSPopUpButton()
    private let chkEnableFilter = FLCheckbox()
    private let btnSetFilter = NSButton()
    private let btnFullScreen = NSButton()
    private let btnOpenLogDir = NSButton()
    private let btnExportLogFile = NSButton()
    // body
    private var table: FLTableView<String, VoidRowCell>! = nil
    // properties
    private var rac_tabScriptSubject: IOSubject<NSButton>!
    var rac_tabScript: IOMonad<NSButton> {
        if rac_tabScriptSubject == nil {
            self.rac_tabScriptSubject = IOSubject<NSButton>()
            self.dataBinding()
        }
        return self.rac_tabScriptSubject.signal
    }
    // inner states
    private let queue = DispatchQueue(label: "__Logger_Queue")
    private var waitDisposer: IODisposer? = nil
    private var dataSource = [LogInfo]()
    private var willShowInfos = [LogInfo]()
    private var notRender = true
    private var logIndex = 0
    
    private func dataBinding() {
        self.selectShowType.addItems(withTitles: LogInfoType.toList())
        self.selectShowType.rac_eventTouch.subscribeNext {[weak self] select in
            if let self = self {
                self.reloadTable()
            }
        }
        self.btnTabScript.rac_eventTouch.subscribeNext {[weak self] _ in
            if let self = self {
                self.rac_tabScriptSubject.sendNext(self.btnTabScript)
            }
        }
        self.btnOpenLogDir.rac_eventTouch.subscribeNext {[weak self] _ in
            if let self = self {
                // TODO: remove, 使用sdksettings
                let filePath = NSHomeDirectory() + "/Library/Caches/Hermes_Mac" + "/Logs/agorasdk.log"
                let dir = NSHomeDirectory() + "/Library/Caches/Hermes_Mac" + "/Logs"
                if FileManager.default.fileExists(atPath: filePath, isDirectory: nil) {
                    NSWorkspace.shared.selectFile(filePath, inFileViewerRootedAtPath: dir)
                }
            }
        }
        RACAssign(self.appContext.preferences).enableFilter = self.chkEnableFilter.rac_eventTouch.map { btn in btn.state == .on }

        self.appContext.engine.addTarget(target: self, runInMainQueue: false)
        self.startMonitorRender()
    }
    
    override func didRemovedFromParent() {
        if !self.notRender {
            self.notRender = true
            self.table = nil
        }
    }
    
    override func didLayoutUI() {
        if self.notRender && self.headerView.subviews.count > 0 {
            self.notRender = false
            if self.dataSource.count > 0 {
                self.reloadTable()
            }
        }
    }
    
    override func layoutUI(needsLayout: Bool, args: [String : Any?]? = nil) {
        self.layoutHeaderView(needsLayout)
        self.layoutBodyView(needsLayout)
    }
    
    private func layoutHeaderView(_ needsLayout: Bool) {
        self.headerView.bind(to: yoga, self.view) { it in
            it.direction = .row
            it.height = 30
            it.alignItems = .center
        }
        self.headerView.flBackgroundColor = .lightGray
        // header-left span
        let leftHeaderSpan = NSView()
        leftHeaderSpan.bind(to: yoga, self.headerView) { it in
            it.flexGrow = 1
            it.height = 30
            it.paddingLeft = 4
            it.direction = .row
            it.alignItems = .center
        }
        leftHeaderSpan.flBackgroundColor = .lightGray
        self.selectShowType.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 30
        }
        self.chkEnableFilter.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.chkEnableFilter.flTitle = "Enable Filter"
        self.chkEnableFilter.flTitleColor = .white
        self.chkEnableFilter.flBackgroundColor = .gray
        self.chkEnableFilter.font = NSFont.boldSystemFont(ofSize: 12)
        self.chkEnableFilter.flCornerRadius = 4
        self.chkEnableFilter.state = self.appContext.preferences.enableFilter ? .on : .off
        
        self.btnSetFilter.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 12
            // TODO: remove
            it.display = .none
        }
        self.btnSetFilter.flTitle = "Set Filter"
        self.btnSetFilter.flTitleColor = .white
        self.btnSetFilter.flBackgroundColor = .gray
        self.btnSetFilter.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnSetFilter.flCornerRadius = 4

        self.btnFullScreen.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.btnFullScreen.flTitle = "Full Screen"
        self.btnFullScreen.flTitleColor = .white
        self.btnFullScreen.flBackgroundColor = .gray
        self.btnFullScreen.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnFullScreen.flCornerRadius = 4
        
        self.btnOpenLogDir.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
        }
        self.btnOpenLogDir.flTitle = "Open Log"
        self.btnOpenLogDir.flTitleColor = .white
        self.btnOpenLogDir.flBackgroundColor = .gray
        self.btnOpenLogDir.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnOpenLogDir.flCornerRadius = 4
        
        self.btnExportLogFile.bind(to: yoga, leftHeaderSpan) { it in
            it.width = 100
            it.height = 26
            it.marginLeft = 4
            // TODO: remove
            it.display = .none
        }
        self.btnExportLogFile.flTitle = "Export Log"
        self.btnExportLogFile.flTitleColor = .white
        self.btnExportLogFile.flBackgroundColor = .gray
        self.btnExportLogFile.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnExportLogFile.flCornerRadius = 4
        // header-right span
        let rightHeaderSpan = NSView()
        rightHeaderSpan.bind(to: yoga, self.headerView) { it in
            it.direction = .row
            it.alignItems = .center
            it.paddingRight = 4
            it.height = 30
        }
        rightHeaderSpan.flBackgroundColor = .lightGray
        self.btnTabScript.bind(to: yoga, rightHeaderSpan) { it in
            it.width = 80
            it.height = 26
        }
        self.btnTabScript.flTitle = "Script Info"
        self.btnTabScript.flTitleColor = .white
        self.btnTabScript.flBackgroundColor = .gray
        self.btnTabScript.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnTabScript.flCornerRadius = 4
        
        self.btnTabLogger.bind(to: yoga, rightHeaderSpan) { it in
            it.width = 80
            it.height = 26
        }
        self.btnTabLogger.flTitle = "Logger Info"
        self.btnTabLogger.flTitleColor = .white
        self.btnTabLogger.flBackgroundColor = .darkGray
        self.btnTabLogger.font = NSFont.boldSystemFont(ofSize: 12)
        self.btnTabLogger.flCornerRadius = 4
    }
    
    private func layoutBodyView(_ needsLayout: Bool) {
        // handle title width
        let indexWidth: Float = 60
        let typeWidth: Float = 60
        let timeWidth: Float = 120
        let remainWidth: Float = Float(self.view.superview!.frame.size.width) -  indexWidth - typeWidth - timeWidth
        let methodWidth: Float = remainWidth * 0.45
        let infoWidth: Float = remainWidth * 0.55
        let methodMinWidth: Float = methodWidth > 550 ? 0 : (infoWidth > 550 ? 550 : 400)
        let methodMaxWidth: Float = methodWidth > 550 ? 550 : 0
        let infoMinWidth = remainWidth - methodMinWidth - methodMaxWidth
        let titles: [(String, Float, Float)] = [("index", 0, indexWidth),
                                                ("type", 0, typeWidth),
                                                ("time", 0, timeWidth),
                                                ("method", methodMinWidth, methodMaxWidth),
                                                ("info", infoMinWidth, 0)]
        self.table = buildColumnsTableView(yoga, columnTitles: titles)
        self.table.bind(to: yoga, self.view) { it in
            it.flexGrow = 1
        }
        self.table.enableAutoShowLatestRows()
    }
}

extension MainLoggerPanelVC : ScriptEngineDispatch {
    private func reloadTable() {
        DispatchQueue.main.async {
            self.table.cleanData(needReload: false)
            var items = [LogInfo]()
            for item in self.dataSource {
                if self.canShowLogType(item.type) {
                    items.append(item)
                }
            }
            let dataItems = items.toDataItems()
            self.table.appendData(columnDatas: dataItems)
        }
    }
    
    private func canShowLogType(_ type: LogInfoType) -> Bool {
        let selectedItem = self.selectShowType.selectedItemTitle
        switch selectedItem {
        case "all":
            return true
        case "api":
            return type == .api
        case "delegate":
            return type == .delegate
        case "other":
            return !(type == .api || type == .delegate)
        default:
            return true
        }
    }
    
    private func logRedirect(type: LogInfoType, engineName: String,
                             isStatic: Bool, selector: String,
                             argValues: [String : Any]?, returnValue: Any?,
                             isSDKInstance: Bool, instanceClass: String) {
        self.queue.async {[weak self] in
            if let self = self {
                self.logIndex += 1
                let index = self.logIndex
                var info: String? = nil
                if type == .api {
                    if let returnValue = returnValue, let method = self.appContext.engine.findAPIMethodNode(by: selector) {
                        if method.declType! == "void" {
                            info = "void"
                        } else {
                            info = dumpAnyToJsonString(value: returnValue, valueType: method.declType,
                                                       engine: self.appContext.engine)
                        }
                    }
                } else if type == .delegate || type == .block {
                    info = dumpAnyToJsonString(forSerialization: false, values: argValues, delegateMethodName: selector,
                                               engine: self.appContext.engine, ignoreSender: true)
                } else {
                    print("TODO: remove, handle custom、app")
                }
                var selectorInfo = selector
                if selector == "setParameters:" {
                    if let json = argValues?["options"] as? String {
                        selectorInfo += json
                    }
                }
                let logInfo = LogInfo(index: index, type, selectorInfo, info ?? "", isStatic, isSDKInstance, instanceClass)
                __logWrite(logInfo.description)
                if !(self.appContext.preferences.enableFilter
                     && self.appContext.preferences.filterSelectors.contains(selector)) {
                    DispatchQueue.main.sync {[weak self] in
                        if let self = self {
                            self.dataSource.append(logInfo)
                            if !self.notRender {
                                if self.waitDisposer == nil {
                                    self.waitDisposer = onceTimer(0.15).subscribeNext({ _ in
                                        self.renderTable()
                                    })
                                }
                                if self.canShowLogType(type) {
                                    self.willShowInfos.append(logInfo)
                                    if self.willShowInfos.count >= 10 {
                                        self.renderTable()
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
    
    private func renderTable() {
        self.waitDisposer?.dispose()
        self.waitDisposer = nil
        if !self.notRender {
            let dataItems = self.willShowInfos.toDataItems()
            self.table.appendData(columnDatas: dataItems)
        }
        self.willShowInfos.removeAll()
    }
    
    func responseDelegate(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                          isSDKInstance: Bool, instanceClass: String) {
        self.logRedirect(type: .delegate, engineName: engineName,
                         isStatic: isSDKInstance, selector: selector,
                         argValues: argValues, returnValue: nil,
                         isSDKInstance: isSDKInstance, instanceClass: instanceClass)
    }
    func responseBlock(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any],
                       isSDKInstance: Bool, instanceClass: String) {
        self.logRedirect(type: .block, engineName: engineName,
                         isStatic: isSDKInstance, selector: selector,
                         argValues: argValues, returnValue: nil,
                         isSDKInstance: isSDKInstance, instanceClass: instanceClass)
    }
    
    func responseAPI(engineName: String, isStatic: Bool, selector: String, argValues: [String : Any], returnValue: Any,
                     isSDKInstance: Bool, instanceClass: String) {
        self.logRedirect(type: .api, engineName: engineName,
                         isStatic: isSDKInstance, selector: selector,
                         argValues: argValues, returnValue: returnValue,
                         isSDKInstance: isSDKInstance, instanceClass: instanceClass)
    }
}
